import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
import requests
import multiprocessing
from bs4 import BeautifulSoup
from shutil import copyfileobj
from urllib import request
import time
import pylightxl as xl
import glob
import datetime
import re
import warnings
import plotly.express as px
import plotly.graph_objects as go
from scipy import stats
from sklearn.model_selection import train_test_split
from statsmodels.tsa.stattools import adfuller
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf, plot_predict
from statsmodels.tsa.arima.model import ARIMA
from tqdm.notebook import tqdm
warnings.filterwarnings('ignore')
def download_excel(url, filename):
''' Télécharge le fichier Excel à l`URL donnée, et le stocke dans le fichier filename '''
with request.urlopen(url) as response, open(filename, 'wb') as out_file:
copyfileobj(response, out_file)
On remarque que les fichiers Excel contenant la liste des dates de classement sont disponibles sur la page principale du site https://www.vendeeglobe.org/fr/classement, et que le champ value des balises option contient le nom du fichier Excel de chaque classement.
Pour récupérer l'ensemble des fichiers Excel, il suffit donc de récupérer les noms de fichier dans les tags option, puis de lancer une requête spécifique pour les télécharger.
html = requests.get("https://www.vendeeglobe.org/fr/classement").content.decode('utf-8')
soup = BeautifulSoup(html)
options = soup.findAll("option")
ranking_dates = []
for opt in options:
if opt["value"]:
ranking_dates.append(opt["value"])
print(f"{len(ranking_dates)} fichiers de classement trouvés.")
params = [(f"https://www.vendeeglobe.org/download-race-data/vendeeglobe_{date}.xlsx", f"vendeeglobe/{date}.xlsx") for date in ranking_dates]
with multiprocessing.Pool() as p:
p.starmap(download_excel, params)
706 fichiers de classement trouvés.
Les fichiers ont été téléchargés en local, on cherche à présent à les importer dans deux Dataframe pandas : un pour les skippers encore en course, l'autre pour les skippers ayant déjà franchis la ligne d'arrivée.
Xlwings ne fonctionnant pas sur mon ordinateur, j'ai utilisé la library pylightxl pour pouvoir ouvrir les fichiers Excel et importer les données sous pandas.
def extract_running_data(datetime, db, row_start, row_stop):
running = []
headers = db.ws(ws="fr").row(row=row_start)[:-1]
headers = [h.split("\n")[0] for h in headers]
headers[0:4] = ["datetime", "rank", "Nat/Voile", "Skipper/Bateau"]
headers[-1:] = ["DTF", "DTL"]
headers[7:11] = [f"last30m_{h}" for h in headers[7:11]]
headers[11:15] = [f"lastranking_{h}" for h in headers[11:15]]
headers[15:19] = [f"last24h_{h}" for h in headers[15:19]]
running.append(headers)
for i in range(row_start+1, row_stop):
skip = db.ws(ws="fr").row(row=i)[1:]
skip.insert(0, datetime)
running.append(skip)
return running
def extract_finished_data(datetime, db, row_stop):
finished = []
headers = db.ws(ws="fr").row(row=5)
headers = [h.split("\n")[0] for h in headers]
headers[0:6] = ["datetime", "rank", "Nat/Voile", "Skipper/Bateau", "Date d'arrivée", "Temps de course"]
headers[12:15:2] = [f"gap_{h}" for h in headers[12:15:2]]
headers[16:18] = [f"ortho_{h}" for h in headers[16:18]]
headers[18] = "percent"
headers[19:21] = [f"ground_{h}" for h in headers[19:21]]
headers = [h for h in headers if h]
finished.append(headers)
for i in range(6, row_stop):
skip = db.ws(ws="fr").row(row=i)[1:]
skip.insert(0, datetime)
skip = [skip[idx] for idx in [0, 1, 2, 3, 7, 8, 13, 15, 16, 17, 18, 19, 20]]
finished.append(skip)
return finished
def extract_data(file):
''' Extracts data from Excel files and stores them into 2 lists of list'''
with open(file, 'rb') as f:
datetime = f.name.split(".")[0].split("/")[1]
db = xl.readxl(f)
# On compte le nombre de fois où "Rang" apparaît dans la colonne B
B = db.ws(ws="fr").col(col=2)
indices = [idx for idx, value in enumerate(B) if value == "Rang\nRank"]
if datetime == "20201108_120200":
#La course n'a pas encore commencé, aucune information utile dans ce fichier
return [], []
if len(indices) > 1 :
# Cas où des skippers ont passé la ligne, mais il y a encore des skippers en course
# Extract les données sur les skippers encore en course
row_start = indices[1] + 2
row_stop = B.index("Traitements et calculs : Géovoile, un service Hauwell Studios", row_start) + 1
running = extract_running_data(datetime, db, row_start, row_stop)
# Extract les données sur les skippers déjà arrivés
finished = extract_finished_data(datetime, db, row_start-1)
elif datetime == "20210305_080000":
# Cas où tous les skippers sont arrivés (ou ont abandonné)
running = []
finished = extract_finished_data(datetime, db, 39)
else:
# Cas où aucun skipper n'a encore franchi la ligne d'arrivée
finished = []
running = extract_running_data(datetime, db, 5, 39)
return finished, running
runnings, arrived = [], []
files = glob.glob("vendeeglobe/*")
for file in files:
finished, running = extract_data(file)
if finished:
dff = pd.DataFrame(finished[1:], columns=finished[0])
arrived.append(dff)
if running:
dfr = pd.DataFrame(running[1:], columns=running[0])
runnings.append(dfr)
df_running = pd.concat(runnings)
df_finished = pd.concat(arrived)
print(f"Classement pour les skippers encore dans la course")
display(df_running.head())
print(f"Classement pour les skippers ayant franchis la ligne d'arrivée")
display(df_finished.head())
Classement pour les skippers encore dans la course
| datetime | rank | Nat/Voile | Skipper/Bateau | Heure FR | Latitude | Longitude | last30m_Cap | last30m_Vitesse | last30m_VMG | ... | lastranking_Cap | lastranking_Vitesse | lastranking_VMG | lastranking_Distance | last24h_Cap | last24h_Vitesse | last24h_VMG | last24h_Distance | DTF | DTL | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 20201230_110000 | 1 | \nFRA 17 | Yannick Bestaven\nMaître Coq IV | 11:30 FR\n | 55°47.17'S | 110°11.00'W | 78° | 16.2 kts | 14.5 kts | ... | 75° | 17.1 kts | 15.1 kts | 51.4 nm | 114° | 12.1 kts | 12.1 kts | 290.0 nm | 8435.6 nm | 0.0 nm |
| 1 | 20201230_110000 | 2 | \nFRA 79 | Charlie Dalin\nAPIVIA | 11:30 FR\n | 53°47.14'S | 112°32.49'W | 85° | 19.6 kts | 17.2 kts | ... | 82° | 21.3 kts | 18.0 kts | 63.8 nm | 85° | 15.0 kts | 12.8 kts | 360.9 nm | 8553.0 nm | 117.4 nm |
| 2 | 20201230_110000 | 3 | \nFRA 59 | Thomas Ruyant\nLinkedOut | 11:30 FR\n | 54°12.00'S | 114°10.44'W | 89° | 19.9 kts | 18.3 kts | ... | 85° | 19.4 kts | 17.4 kts | 58.3 nm | 92° | 15.6 kts | 14.5 kts | 374.4 nm | 8596.2 nm | 160.6 nm |
| 3 | 20201230_110000 | 4 | \nFRA 1000 | Damien Seguin\nGroupe APICIL | 11:30 FR\n | 54°58.86'S | 114°41.63'W | 90° | 17.7 kts | 16.8 kts | ... | 92° | 17.3 kts | 16.6 kts | 51.8 nm | 93° | 15.4 kts | 14.6 kts | 369.6 nm | 8596.8 nm | 161.2 nm |
| 4 | 20201230_110000 | 5 | \nFRA 01 | Jean Le Cam\nYes we Cam ! | 11:30 FR\n | 54°58.66'S | 116°27.85'W | 95° | 9.0 kts | 8.8 kts | ... | 96° | 10.1 kts | 9.9 kts | 30.4 nm | 92° | 14.0 kts | 13.3 kts | 336.5 nm | 8654.7 nm | 219.1 nm |
5 rows × 21 columns
Classement pour les skippers ayant franchis la ligne d'arrivée
| datetime | rank | Nat/Voile | Skipper/Bateau | Date d'arrivée | Temps de course | gap_Au premier | gap_Au précédent | ortho_Vitesse | ortho_Distance | percent | ground_Vitesse | ground_Distance | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 20210226_170000 | 1\nARV | \nFRA 17 | Yannick Bestaven\nMaître Coq IV | 28/01/2021 04:19:46 FR | 80j 03h 44min 46s\n-10h 15min 00s | 12.6 kts | 24365.7 nm | 117.3 % | 14.8 kts | 28583.8 nm | ||
| 1 | 20210226_170000 | 2\nARV | \nFRA 79 | Charlie Dalin\nAPIVIA | 27/01/2021 20:35:47 FR | 80j 06h 15min 47s\n | 02h 31min 01s | 02h 31min 01s | 12.6 kts | 24365.7 nm | 119.6 % | 15.1 kts | 29135.0 nm |
| 2 | 20210226_170000 | 3\nARV | \nFRA 18 | Louis Burton\nBureau Vallée 2 | 28/01/2021 00:45:12 FR | 80j 10h 25min 12s\n | 06h 40min 26s | 04h 09min 25s | 12.6 kts | 24365.7 nm | 117.6 % | 14.8 kts | 28650.0 nm |
| 3 | 20210226_170000 | 4\nARV | \nFRA 01 | Jean Le Cam\nYes we Cam ! | 28/01/2021 20:19:55 FR | 80j 13h 44min 55s\n-16h 15min 00s | 10h 00min 09s | 03h 19min 43s | 12.5 kts | 24365.7 nm | 112.9 % | 14.1 kts | 27501.5 nm |
| 4 | 20210226_170000 | 5\nARV | \nMON 10 | Boris Herrmann\nSeaexplorer - Yacht Club De Mo... | 28/01/2021 11:19:45 FR | 80j 14h 59min 45s\n-06h 00min 00s | 11h 14min 59s | 01h 14min 50s | 12.6 kts | 24365.7 nm | 116.8 % | 14.7 kts | 28448.5 nm |
La page listant les caractéristiques des bateaux est https://www.vendeeglobe.org/fr/glossaire
html = requests.get("https://www.vendeeglobe.org/fr/glossaire").content.decode('utf-8')
soup = BeautifulSoup(html)
boats = []
popups = soup.findAll("div", attrs={"class": "sv-u-1 sv-u-s-1-2 sv-u-m-1-3 sv-u-l-1-6"})
for elem in popups:
boat = {}
boat["skipper_name"] = elem.find("span", attrs={"class": "boats-list__skipper-name"}).text
skipper_id = elem.find_next("div", attrs={"class": "boats-list__popup mfp-hide"})["id"]
boat["skipper_id"] = skipper_id.split("-")[-1]
boat["Nom du bateau"] = elem.find_next('h3').text
spec_list = elem.find_next('ul').find_all('li')
for spec in spec_list:
key, value = spec.text.split(":")[:2]
if " / Assemblage" in value:
value = value.split("/")[0]
boat[key.strip()] = value.strip()
boats.append(boat)
df_boats = pd.DataFrame(boats)
df_boats
| skipper_name | skipper_id | Nom du bateau | Numéro de voile | Anciens noms du bateau | Architecte | Chantier | Date de lancement | Longueur | Largeur | Tirant d'eau | Déplacement (poids) | Nombre de dérives | Hauteur mât | Voile quille | Surface de voiles au près | Surface de voiles au portant | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Fabrice AMEDEO | 93 | NEWREST - ART & FENÊTRES | FRA 56 | No Way Back, Vento di Sardegna | VPLP/Verdier | Persico Marine | 01 Août 2015 | 18,28 m | 5,85 m | 4,50 m | 7 t | foils | 29 m | monotype | 320 m2 | 570 m2 |
| 1 | Romain ATTANASIO | 110 | PURE - Best Western® | FRA 49 | Gitana Eighty, Synerciel, Newrest-Matmut | Bruce Farr Design | Southern Ocean Marine (Nouvelle Zélande) | 08 Mars 2007 | 18,28m | 5,80m | 4,50m | 9t | 2 | 28m | acier forgé | 280 m2 | 560 m2 |
| 2 | Alexia BARRIER | 100 | TSE - 4MYPLANET | FRA72 | Famille Mary-Etamine du Lys, Initiatives Coeur... | Marc Lombard | MAG France | 01 Mars 1998 | 18,28m | 5,54m | 4,50m | 9t | 2 | 29 m | acier | 260 m2 | 580 m2 |
| 3 | Yannick BESTAVEN | 91 | Maître CoQ IV | 17 | Safran 2 - Des Voiles et Vous | Verdier - VPLP | CDK Technologies | 12 Mars 2015 | 18,28 m | 5,80 m | 4,50 m | 8 t | foils | 29 m | acier mécano soudé | 310 m2 | 550 m2 |
| 4 | Jérémie BEYOU | 103 | CHARAL | 08 | NaN | VPLP | CDK Technologies | 18 Août 2018 | 18,28 m | 5,85 m | 4,50 m | 8t | foils | 29 m | acier | 320 m2 | 600 m2 |
| 5 | Arnaud BOISSIÈRES | 88 | LA MIE CÂLINE - ARTISANS ARTIPÔLE | FRA 14 | Ecover3, Président, Gamesa, Kilcullen Voyager-... | Owen Clarke Design LLP - Clay Oliver | Hakes Marine - Mer Agitée | 03 Août 2007 | 18,28 m | 5,65 m | 4,50 m | 7,9 tonnes | foils | 29 m | basculante avec vérin | 300 m² | 610 m² |
| 6 | Louis BURTON | 106 | BUREAU VALLEE 2 | 18 | Banque Populaire VIII | Verdier - VPLP | CDK Technologies | 09 Juin 2015 | 18,28 m | 5,80 m | 4,50 m | 7,6 t | foils | 28 m | acier | 300 m2 | 600 m2 |
| 7 | Didac COSTA | 123 | ONE PLANET ONE OCEAN | ESP 33 | Kingfisher - Educacion sin Fronteras - Forum M... | Owen Clarke Design | Martens Yachts | 02 Février 2000 | 18,28 m | 5,30 m | 4,50 m | 8,9 t | 2 | 26 m | acier | 240 m2 | 470 m2 |
| 8 | Manuel COUSIN | 92 | GROUPE SÉTIN | FRA 71 | Paprec-Virbac2, Estrella Damm, We are Water, L... | Bruce Farr Yacht Design | Southern Ocean Marine (Nouvelle-Zélande) | 02 Février 2007 | 18,28 m | 5,80 m | 4,50 m | 9 t | 2 asymétriques | 28,50 | basculante sur vérin hydraulique | 270 m2 | 560 m2 |
| 9 | Clarisse CREMER | 95 | BANQUE POPULAIRE X | FRA30 | Macif - SMA | Verdier - VPLP | CDK - Mer Agitée | 01 Mars 2011 | 18,28 m | 5,70 m | 4,5 m | 7,7 t | 2 | 29 m | acier forgé | 340 m2 | 570 m2 |
| 10 | Charlie DALIN | 105 | APIVIA | FRA 79 | NaN | Verdier | CDK technologies - MerConcept | 05 Août 2019 | 18,28 m | 5,85 m | 4,50 m | 8 t | foils | 29 m | acier | 350 m2 | 560 m2 |
| 11 | Sam DAVIES | 97 | INITIATIVES-COEUR | FRA109 | Maître CoQ - Banque Populaire VII- Foncia II | VPLP - Verdier | CDK Technologies | 20 Septembre 2010 | 18,28 m | 5,70 m | 4,50 m | 7,8 t | foils | 27 m | acier forgé | 300 m2 | 600 m2 |
| 12 | Sébastien DESTREMAU | 117 | MERCI | 69 | Foresight Natural Energy, Maisonneuve | Lavanos | Artech do Brasil | 15 Janvier 2005 | 18,28 m | 5,60 m | 4,50 m | 8,5 t | 2 | 29 m | acier | 250 m2 | 650 m2 |
| 13 | Benjamin DUTREUX | 112 | OMIA - WATER FAMILY | FRA09 | Spirit of Yukoh, Neutrogena, Hugo Boss, Estrel... | Bruce Farr Design | Offshore Challenge - Cowes | 03 Juillet 2007 | 18,28 m | 5,85 m | 4,50 m | 8 t | 2 | 29 m | acier forgé | 300 m2 | 700 m2 |
| 14 | Kevin ESCOFFIER | 89 | PRB | FRA 85 | NaN | Verdier - VPLP | CDK Technologies | 08 Mars 2010 | 18,28 m | 5,50 m | 4,50 m | NC | foils | 27,40 m | Acier mécano soudé | 300 m2 | 600 m2 |
| 15 | Clément GIRAUD | 114 | Compagnie du Lit / Jiliti | FRA83 | Delta Dore, Bureau Vallée, Vers un Monde sans ... | Bruce Farr design | JMV Cherbourg | 26 Juillet 2006 | 18,28 m | 5,75 m | 4,50 m | 8,5 t | 2 | 29 m | acier forgé | 300 m2 | 620 m2 |
| 16 | François Guiffant | 129 | NaN | NaN | NaN | NaN | 01 Janvier 1970 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |
| 17 | Pip HARE | 120 | MEDALLIA | GBR77 | Armor Lux, We Are Water, La Fabrique | Pierre Rolland | Bernard Stamm | 03 Juillet 1999 | 18,28 m | 5,70 m | 4,50 m | 9 t | 2 | 29 m | carbone | 300 m2 | 580 m2 |
| 18 | Boris HERRMANN | 109 | SEAEXPLORER - YACHT CLUB DE MONACO | 16 | Edmond de Rothschild, Malizia - Yacht Club de ... | Verdier - VPLP | Multiplast | 07 Août 2015 | 18,28 m | 5,70 m | 4,50 m | 7,6 t | foils | 29 m | acier | 290 m2 | 490 m2 |
| 19 | Ari HUUSELA | 124 | STARK | FIN222 | Aviva, GAES Centros Auditivos | Owen Clarke Design | Hakes Marine - Wellington (Nouvelle-Zélande) | 06 Août 2007 | 18,28 m | 5,80 m | 4,50 m | 8,5 t | 2 | 28 m | acier | 270 m2 | 580 m2 |
| 20 | Isabelle JOSCHKE | 102 | MACSF | FRA 27 | Quéguiner, Safran | Verdier - VPLP | Chantier Naval de Larros | 04 Juillet 2007 | 18,28 m | 5,60 m | 4,50 m | 7,7 t | foils | 27 m | acier | 300 m2 | 650 m2 |
| 21 | Jean LE CAM | 116 | Yes We Cam! | 001 | Cheminées Poujoulat, Mare, Maître CoQ, Mapfre,... | Bruce Farr design | CDK Technologies | 03 Janvier 2007 | 18,28 m | 5,90 m | 4,50 m | 8 t | 2 | 28 m | acier forgé | 300 m2 | 620 m2 |
| 22 | Stéphane LE DIRAISON | 94 | TIME FOR OCEANS | FRA 92 | Hugo Boss, Energa, Compagnie du Lit-Boulogne B... | Finot-Conq Design | Neville Hutton | 01 Juin 2007 | 18,28 m | 5,84 m | 4,50 m | 8,5 t | foils | 28 m | monotype | 300 m2 | 550 m2 |
| 23 | Miranda MERRON | 104 | CAMPAGNE DE FRANCE | FRA50 | Great America IV, Mirabaud, Temenos | Owen Clarke | Southern Ocean Marine, Tauranga | 04 Mai 2006 | 18,28 m | 5,50 m | 4,50 m | 8,5 t | 2 | 28 m | carbone | 330 m2 | 600 m2 |
| 24 | Giancarlo PEDOTE | 96 | PRYSMIAN GROUP | ITA 34 | Saint-Michel - Virbac | VPLP - Verdier | Multiplast | 02 Avril 2015 | 18,28 m | 5,80 m | 4,5 m | 8 t | foils | 29 m | acier forgé | 300 m2 | 600 m2 |
| 25 | Alan ROURA | 99 | LA FABRIQUE | SUI07 | Brit Air, Votre Nom autour du Monde, MACSF | Groupe Finot-Conq | Multiplast | 01 Août 2007 | 18,28 m | 5,90 m | 4,50 m | nc | foils | 27 m | carbone | 290 m2 | 580 m2 |
| 26 | Thomas RUYANT | 113 | LinkedOut | NaN | NaN | Verdier | Persico | 03 Septembre 2019 | 18,28 m | 5,85 m | 4,50 m | 8 t | foils | 29 m | acier forgé | 350 m2 | 560 m2 |
| 27 | Damien SEGUIN | 111 | GROUPE APICIL | FRA1000 | Comme Un Seul Homme, DCNS | Groupe Finot-Conq | Multiplast | 10 Août 2008 | 18,28 m | 5,85 m | 4,50 m | 8,5 t | 2 | 29 m | acier forgé | 350 m2 | 610 m2 |
| 28 | Kojiro SHIRAISHI | 119 | DMG MORI Global One | JPN 11 | NaN | VPLP | Multiplast | 05 Septembre 2019 | 18,28 m | 5,85 m | 4,50 m | 8 t | foils | 29 m | acier forgé | 320 m2 | 580 m2 |
| 29 | Sébastien SIMON | 90 | ARKEA PAPREC | 4 | NaN | Juan Kouyoumdjian | CDK Technologies | 19 Juillet 2019 | 18,28 m | 5,70 m | 4,50 m | 8 t | foiler | 29 m | Inox usiné | 260 m2 | 600 m2 |
| 30 | Maxime SOREL | 107 | V and B-MAYENNE | FRA53 | Le Souffle du Nord, Groupe Bel | Verdier - VPLP | Indiana Yachting (Scarlino, Italie) | 07 Septembre 2007 | 18,28 m | 5,50 m | 4,50 m | 7,7 t | 2 | 29 m | acier | 365 m2 | 700 m2 |
| 31 | Alex THOMSON | 122 | HUGO BOSS | GBR 99 | NaN | VPLP - Alex Thomson Racing (led by Pete Hobson) | Carrington Boats | 15 Août 2019 | 18,28 m | 5,40 m | 4,50 m | 7,7 t | foils | 29 m | acier forgé | 330 m2 | 630 m2 |
| 32 | Armel TRIPON | 101 | L'OCCITANE EN PROVENCE | 2 | NaN | Samuel Manuard | Black Pepper© | 31 Janvier 2020 | 18,28 m | 5,50 m | 4,50 m | 7,8 t | foils | 28 m | acier forgé | 270 m2 | 535 m2 |
| 33 | Nicolas TROUSSEL | 108 | CORUM L'EPARGNE | FRA 6 | NaN | Juan Kouyoumdjian | CDK technologies - Mer Agitée | 15 Mai 2020 | 18,28 m | 5,70 m | 4,50 m | 7,9 t | foils | 27,30 m | NaN | 270 m2 | 535 m2 |
def clean_ranking_running(df):
# Formattage des dates
df["datetime"] = pd.to_datetime(df["datetime"], format="%Y%m%d_%H%M00")
# Convertit le rang en chiffre pour pouvoir faire des moyennes dessus
# Les valeurs non numériques pour les abandons / retards au départ sont converties en Nan
df["rank"] = pd.to_numeric(df['rank'], errors="coerce").astype(float)
# Séparation des nationalités et voiles
# Le champ Nationalité est presque toujours vide et non utile, il n'est pas gardé
df.insert(3, "Voile", df['Nat/Voile'].apply(lambda x: x.split("\n")[1]))
# Séparation des skippers et bateaux
df.insert(4, "Skipper", df['Skipper/Bateau'].apply(lambda x: x.split("\n")[0]))
df.insert(5, "Bateau", df['Skipper/Bateau'].apply(lambda x: x.split("\n")[1]))
df.drop(columns=['Nat/Voile', 'Skipper/Bateau', 'Heure FR'], inplace=True)
# Conversion des coordonnées en valeurs décimales
def convert_degrees(s):
if s != "":
degrees = re.split('[°\']', s)
return (float(degrees[0]) + (float(degrees[1])/60)) * (-1 if degrees[2] == 'S' or degrees[2] == 'W' else 1)
df['Latitude'] = df['Latitude'].apply(convert_degrees)
df['Longitude'] = df['Longitude'].apply(convert_degrees)
# Conversion des grandeurs physiques en nombres
for col in ['last30m_Cap', 'lastranking_Cap', 'last24h_Cap']:
df[col] = df[col].astype(str).str[:-1].astype(float, errors="ignore")
for col in ['last30m_Vitesse', 'lastranking_Vitesse', 'last24h_Vitesse',
'last30m_VMG', 'lastranking_VMG', 'last24h_VMG',
'last30m_Distance', 'lastranking_Distance', 'last24h_Distance',
'DTF', 'DTL']:
#df[col] = df[col].apply(lambda x: x.split(' ')[0]).astype(float, errors="ignore")
df[col] = pd.to_numeric(df[col].apply(lambda x: x.split(' ')[0]), errors="coerce").astype(float)
# Tri par ordre de dates et rang
df = df.sort_values(by=["datetime", "rank"], ascending=[True,True], axis=0, ignore_index=True)
return df
df_running = clean_ranking_running(df_running)
df_running
| datetime | rank | Voile | Skipper | Bateau | Latitude | Longitude | last30m_Cap | last30m_Vitesse | last30m_VMG | ... | lastranking_Cap | lastranking_Vitesse | lastranking_VMG | lastranking_Distance | last24h_Cap | last24h_Vitesse | last24h_VMG | last24h_Distance | DTF | DTL | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2020-11-08 14:00:00 | 1.0 | FRA 18 | Louis Burton | Bureau Vallée 2 | 46.407667 | -1.841333 | 241 | 17.7 | 17.5 | ... | 357 | 0.0 | 0.0 | 2788.0 | 201 | 0.3 | 0.3 | 6.1 | 24293.9 | 0.0 |
| 1 | 2020-11-08 14:00:00 | 2.0 | MON 10 | Boris Herrmann | Seaexplorer - Yacht Club De Monaco | 46.405667 | -1.830333 | 241 | 11.1 | 10.9 | ... | 357 | 0.0 | 0.0 | 2787.9 | 196 | 0.3 | 0.2 | 6.0 | 24294.2 | 0.4 |
| 2 | 2020-11-08 14:00:00 | 3.0 | FRA 8 | Jérémie Beyou | Charal | 46.415167 | -1.833167 | 244 | 15.5 | 15.5 | ... | 357 | 0.0 | 0.0 | 2788.5 | 199 | 0.2 | 0.2 | 5.5 | 24294.3 | 0.5 |
| 3 | 2020-11-08 14:00:00 | 4.0 | FRA 59 | Thomas Ruyant | LinkedOut | 46.411833 | -1.828000 | 244 | 13.2 | 13.1 | ... | 357 | 0.0 | 0.0 | 2788.3 | 196 | 0.2 | 0.2 | 5.6 | 24294.5 | 0.6 |
| 4 | 2020-11-08 14:00:00 | 5.0 | FRA 53 | Maxime Sorel | V And B Mayenne | 46.409833 | -1.826000 | 246 | 10.9 | 10.9 | ... | 357 | 0.0 | 0.0 | 2788.1 | 195 | 0.8 | 0.7 | 5.8 | 24294.5 | 0.6 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 19222 | 2021-03-05 04:00:00 | NaN | FRA 109 | Samantha Davies | Initiatives - Coeur | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |||
| 19223 | 2021-03-05 04:00:00 | NaN | FRA 4 | Sébastien Simon | ARKEA PAPREC | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |||
| 19224 | 2021-03-05 04:00:00 | NaN | GBR 99 | Alex Thomson | HUGO BOSS | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |||
| 19225 | 2021-03-05 04:00:00 | NaN | FRA 85 | Kevin Escoffier | PRB | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |||
| 19226 | 2021-03-05 04:00:00 | NaN | FRA 6 | Nicolas Troussel | CORUM L'Épargne | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
19227 rows × 21 columns
def clean_ranking_finished(df):
# Formattage des dates
df["datetime"] = pd.to_datetime(df["datetime"], format="%Y%m%d_%H%M00")
df["Date d'arrivée"] = df["Date d'arrivée"].apply(lambda x: pd.to_datetime(x.split(" FR")[0], format="%d/%m/%Y %H:%M:%S") if x!= "" else x)
df["Temps de course"] = df["Temps de course"].apply(lambda x: x.split("\n")[0] if "\n" in x else x)
# Séparation des rangs et statuts
df.insert(1, "Rang", pd.to_numeric(df['rank'].apply(lambda x: x.split("\n")[0]), errors="coerce").astype(float))
df.insert(2, "Statut", df['rank'].apply(lambda x: x.split("\n")[1] if "\n" in x else x))
# Séparation des nationalités et voiles
# Le champ Nationalité est presque toujours vide et non utile, il n'est pas gardé
df.insert(3, "Voile", df['Nat/Voile'].apply(lambda x: x.split("\n")[1]))
# Séparation des skippers et bateaux
df.insert(4, "Skipper", df['Skipper/Bateau'].apply(lambda x: x.split("\n")[0]))
df.insert(5, "Bateau", df['Skipper/Bateau'].apply(lambda x: x.split("\n")[1]))
df.drop(columns=['rank', 'Nat/Voile', 'Skipper/Bateau'], inplace=True)
# Conversion des grandeurs physiques en nombres
for col in ['ortho_Vitesse', 'ortho_Distance', 'percent', 'ground_Vitesse', 'ground_Distance']:
#df[col] = df[col].apply(lambda x: x.split(' ')[0]).astype(float, errors='ignore')
df[col] = pd.to_numeric(df[col].apply(lambda x: x.split(' ')[0]), errors="coerce").astype(float)
# Tri par ordre de dates et rang
df = df.sort_values(by=["datetime", "Rang"], ascending=[True,True], axis=0, ignore_index=True)
return df
df_finished = clean_ranking_finished(df_finished)
df_finished
| datetime | Rang | Statut | Voile | Skipper | Bateau | Date d'arrivée | Temps de course | gap_Au premier | gap_Au précédent | ortho_Vitesse | ortho_Distance | percent | ground_Vitesse | ground_Distance | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2021-01-27 17:00:00 | 1.0 | ARV | FRA 79 | Charlie Dalin | APIVIA | 2021-01-27 21:35:47 | 80j 06h 15min 47s | 12.6 | 24355.0 | 119.6 | 15.1 | 29135.0 | ||
| 1 | 2021-01-27 21:00:00 | 1.0 | ARV | FRA 17 | Yannick Bestaven | Maître Coq IV | 2021-01-28 05:19:46 | 80j 03h 44min 46s | 12.6 | 24365.7 | 117.3 | 14.8 | 28583.8 | ||
| 2 | 2021-01-27 21:00:00 | 2.0 | ARV | FRA 79 | Charlie Dalin | APIVIA | 2021-01-27 21:35:47 | 80j 06h 15min 47s | 02h 31min 01s | 02h 31min 01s | 12.6 | 24365.7 | 119.6 | 15.1 | 29135.0 |
| 3 | 2021-01-27 21:00:00 | 3.0 | ARV | FRA 18 | Louis Burton | Bureau Vallée 2 | 2021-01-28 01:45:12 | 80j 10h 25min 12s | 06h 40min 26s | 04h 09min 25s | 12.6 | 24365.7 | 117.6 | 14.8 | 28650.0 |
| 4 | 2021-01-28 04:00:00 | 1.0 | ARV | FRA 17 | Yannick Bestaven | Maître Coq IV | 2021-01-28 05:19:46 | 80j 03h 44min 46s | 12.6 | 24365.7 | 117.3 | 14.8 | 28583.8 | ||
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 3934 | 2021-03-05 08:00:00 | NaN | RET | FRA 109 | Samantha Davies | Initiatives - Coeur | NaT | NaN | NaN | NaN | NaN | NaN | |||
| 3935 | 2021-03-05 08:00:00 | NaN | RET | FRA 4 | Sébastien Simon | ARKEA PAPREC | NaT | NaN | NaN | NaN | NaN | NaN | |||
| 3936 | 2021-03-05 08:00:00 | NaN | RET | GBR 99 | Alex Thomson | HUGO BOSS | NaT | NaN | NaN | NaN | NaN | NaN | |||
| 3937 | 2021-03-05 08:00:00 | NaN | RET | FRA 85 | Kevin Escoffier | PRB | NaT | NaN | NaN | NaN | NaN | NaN | |||
| 3938 | 2021-03-05 08:00:00 | NaN | RET | FRA 6 | Nicolas Troussel | CORUM L'Épargne | NaT | NaN | NaN | NaN | NaN | NaN |
3939 rows × 15 columns
Les noms des skippers ne sont pas orthographiés de la même manière entre les données de classement et de bateau. On les extrait des données de classement pour faire un mapping dessus dans la table des bateaux
skippers = df_finished["Skipper"].unique().tolist()
first_names = [name.split(" ")[0] for name in skippers]
names, counts = np.unique(first_names, return_counts=True)
print(f"Prénoms en double : {names[np.where(counts > 1)][0]}")
Prénoms en double : Sébastien
Seul le prénom Sébastien est en double, se baser sur les prénoms pour faire le mapping devrait donc se faire sans trop de problèmes.
Dans le dataframe des bateaux, un des bateaux n'a pas de numéro de voile. Peut-être peut-on le récupérer depuis le dataframe des classements
voiles = df_finished[["Skipper", "Voile"]]
voiles[voiles["Skipper"]=="Thomas Ruyant"].iloc[0]
Skipper Thomas Ruyant Voile FRA 59 Name: 7, dtype: object
Le numéro de voile correspondant est donc FRA 59
Certains Nan sont présents dans des champs non utiles (ex: Anciens noms du bateau), ils ne sont donc pas nettoyés explicitement.
def clean_boats_features(df_boats, skipper_mapping):
# Drop François Guiffant (seulement un nom, aucune caractéristique de bateau, n'intervient pas dans la course)
df_boats = df_boats.drop(16).reset_index(drop=True)
# Conversion des grandeurs physiques en nombres
for col in ['Longueur', 'Largeur', 'Tirant d\'eau', 'Déplacement (poids)', 'Hauteur mât', 'Surface de voiles au près', 'Surface de voiles au portant']:
df_boats[col] = df_boats[col].apply(lambda x: re.sub(' ?[A-Za-z]+[2²]?', '', x).replace(',', '.')).astype(float, errors='ignore')
# Mapping des noms avec ceux des données de classement pour merge
mapping = {}
skippers = df_boats["skipper_name"].unique().tolist()
for skip in skippers:
if skip == "Sébastien DESTREMAU":
mapping[skip] = "Sébastien Destremau"
elif skip == "Sébastien SIMON":
mapping[skip] = "Sébastien Simon"
elif skip == "Sam DAVIES":
mapping[skip] = "Samantha Davies"
else:
mapping[skip] = [name for name in skipper_mapping if name.split(" ")[0] == skip.split(" ")[0]][0]
df_boats["skipper_name"] = df_boats["skipper_name"].map(mapping)
df_boats[df_boats["skipper_name"] == "Thomas Ruyant"]["Numéro de voile"] = "FRA 59"
# Fix foils bateau SIMON
df_boats = df_boats.replace("foiler", "foils")
return df_boats
df_boats = clean_boats_features(df_boats, skippers)
df_boats
| skipper_name | skipper_id | Nom du bateau | Numéro de voile | Anciens noms du bateau | Architecte | Chantier | Date de lancement | Longueur | Largeur | Tirant d'eau | Déplacement (poids) | Nombre de dérives | Hauteur mât | Voile quille | Surface de voiles au près | Surface de voiles au portant | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Fabrice Amedeo | 93 | NEWREST - ART & FENÊTRES | FRA 56 | No Way Back, Vento di Sardegna | VPLP/Verdier | Persico Marine | 01 Août 2015 | 18.28 | 5.85 | 4.5 | 7 | foils | 29.0 | monotype | 320.0 | 570.0 |
| 1 | Romain Attanasio | 110 | PURE - Best Western® | FRA 49 | Gitana Eighty, Synerciel, Newrest-Matmut | Bruce Farr Design | Southern Ocean Marine (Nouvelle Zélande) | 08 Mars 2007 | 18.28 | 5.80 | 4.5 | 9 | 2 | 28.0 | acier forgé | 280.0 | 560.0 |
| 2 | Alexia Barrier | 100 | TSE - 4MYPLANET | FRA72 | Famille Mary-Etamine du Lys, Initiatives Coeur... | Marc Lombard | MAG France | 01 Mars 1998 | 18.28 | 5.54 | 4.5 | 9 | 2 | 29.0 | acier | 260.0 | 580.0 |
| 3 | Yannick Bestaven | 91 | Maître CoQ IV | 17 | Safran 2 - Des Voiles et Vous | Verdier - VPLP | CDK Technologies | 12 Mars 2015 | 18.28 | 5.80 | 4.5 | 8 | foils | 29.0 | acier mécano soudé | 310.0 | 550.0 |
| 4 | Jérémie Beyou | 103 | CHARAL | 08 | NaN | VPLP | CDK Technologies | 18 Août 2018 | 18.28 | 5.85 | 4.5 | 8 | foils | 29.0 | acier | 320.0 | 600.0 |
| 5 | Arnaud Boissieres | 88 | LA MIE CÂLINE - ARTISANS ARTIPÔLE | FRA 14 | Ecover3, Président, Gamesa, Kilcullen Voyager-... | Owen Clarke Design LLP - Clay Oliver | Hakes Marine - Mer Agitée | 03 Août 2007 | 18.28 | 5.65 | 4.5 | 7.9 | foils | 29.0 | basculante avec vérin | 300.0 | 610.0 |
| 6 | Louis Burton | 106 | BUREAU VALLEE 2 | 18 | Banque Populaire VIII | Verdier - VPLP | CDK Technologies | 09 Juin 2015 | 18.28 | 5.80 | 4.5 | 7.6 | foils | 28.0 | acier | 300.0 | 600.0 |
| 7 | Didac Costa | 123 | ONE PLANET ONE OCEAN | ESP 33 | Kingfisher - Educacion sin Fronteras - Forum M... | Owen Clarke Design | Martens Yachts | 02 Février 2000 | 18.28 | 5.30 | 4.5 | 8.9 | 2 | 26.0 | acier | 240.0 | 470.0 |
| 8 | Manuel Cousin | 92 | GROUPE SÉTIN | FRA 71 | Paprec-Virbac2, Estrella Damm, We are Water, L... | Bruce Farr Yacht Design | Southern Ocean Marine (Nouvelle-Zélande) | 02 Février 2007 | 18.28 | 5.80 | 4.5 | 9 | 2 asymétriques | 28.5 | basculante sur vérin hydraulique | 270.0 | 560.0 |
| 9 | Clarisse Cremer | 95 | BANQUE POPULAIRE X | FRA30 | Macif - SMA | Verdier - VPLP | CDK - Mer Agitée | 01 Mars 2011 | 18.28 | 5.70 | 4.5 | 7.7 | 2 | 29.0 | acier forgé | 340.0 | 570.0 |
| 10 | Charlie Dalin | 105 | APIVIA | FRA 79 | NaN | Verdier | CDK technologies - MerConcept | 05 Août 2019 | 18.28 | 5.85 | 4.5 | 8 | foils | 29.0 | acier | 350.0 | 560.0 |
| 11 | Samantha Davies | 97 | INITIATIVES-COEUR | FRA109 | Maître CoQ - Banque Populaire VII- Foncia II | VPLP - Verdier | CDK Technologies | 20 Septembre 2010 | 18.28 | 5.70 | 4.5 | 7.8 | foils | 27.0 | acier forgé | 300.0 | 600.0 |
| 12 | Sébastien Destremau | 117 | MERCI | 69 | Foresight Natural Energy, Maisonneuve | Lavanos | Artech do Brasil | 15 Janvier 2005 | 18.28 | 5.60 | 4.5 | 8.5 | 2 | 29.0 | acier | 250.0 | 650.0 |
| 13 | Benjamin Dutreux | 112 | OMIA - WATER FAMILY | FRA09 | Spirit of Yukoh, Neutrogena, Hugo Boss, Estrel... | Bruce Farr Design | Offshore Challenge - Cowes | 03 Juillet 2007 | 18.28 | 5.85 | 4.5 | 8 | 2 | 29.0 | acier forgé | 300.0 | 700.0 |
| 14 | Kevin Escoffier | 89 | PRB | FRA 85 | NaN | Verdier - VPLP | CDK Technologies | 08 Mars 2010 | 18.28 | 5.50 | 4.5 | foils | 27.4 | Acier mécano soudé | 300.0 | 600.0 | |
| 15 | Clément Giraud | 114 | Compagnie du Lit / Jiliti | FRA83 | Delta Dore, Bureau Vallée, Vers un Monde sans ... | Bruce Farr design | JMV Cherbourg | 26 Juillet 2006 | 18.28 | 5.75 | 4.5 | 8.5 | 2 | 29.0 | acier forgé | 300.0 | 620.0 |
| 16 | Pip Hare | 120 | MEDALLIA | GBR77 | Armor Lux, We Are Water, La Fabrique | Pierre Rolland | Bernard Stamm | 03 Juillet 1999 | 18.28 | 5.70 | 4.5 | 9 | 2 | 29.0 | carbone | 300.0 | 580.0 |
| 17 | Boris Herrmann | 109 | SEAEXPLORER - YACHT CLUB DE MONACO | 16 | Edmond de Rothschild, Malizia - Yacht Club de ... | Verdier - VPLP | Multiplast | 07 Août 2015 | 18.28 | 5.70 | 4.5 | 7.6 | foils | 29.0 | acier | 290.0 | 490.0 |
| 18 | Ari Huusela | 124 | STARK | FIN222 | Aviva, GAES Centros Auditivos | Owen Clarke Design | Hakes Marine - Wellington (Nouvelle-Zélande) | 06 Août 2007 | 18.28 | 5.80 | 4.5 | 8.5 | 2 | 28.0 | acier | 270.0 | 580.0 |
| 19 | Isabelle Joschke | 102 | MACSF | FRA 27 | Quéguiner, Safran | Verdier - VPLP | Chantier Naval de Larros | 04 Juillet 2007 | 18.28 | 5.60 | 4.5 | 7.7 | foils | 27.0 | acier | 300.0 | 650.0 |
| 20 | Jean Le Cam | 116 | Yes We Cam! | 001 | Cheminées Poujoulat, Mare, Maître CoQ, Mapfre,... | Bruce Farr design | CDK Technologies | 03 Janvier 2007 | 18.28 | 5.90 | 4.5 | 8 | 2 | 28.0 | acier forgé | 300.0 | 620.0 |
| 21 | Stéphane Le Diraison | 94 | TIME FOR OCEANS | FRA 92 | Hugo Boss, Energa, Compagnie du Lit-Boulogne B... | Finot-Conq Design | Neville Hutton | 01 Juin 2007 | 18.28 | 5.84 | 4.5 | 8.5 | foils | 28.0 | monotype | 300.0 | 550.0 |
| 22 | Miranda Merron | 104 | CAMPAGNE DE FRANCE | FRA50 | Great America IV, Mirabaud, Temenos | Owen Clarke | Southern Ocean Marine, Tauranga | 04 Mai 2006 | 18.28 | 5.50 | 4.5 | 8.5 | 2 | 28.0 | carbone | 330.0 | 600.0 |
| 23 | Giancarlo Pedote | 96 | PRYSMIAN GROUP | ITA 34 | Saint-Michel - Virbac | VPLP - Verdier | Multiplast | 02 Avril 2015 | 18.28 | 5.80 | 4.5 | 8 | foils | 29.0 | acier forgé | 300.0 | 600.0 |
| 24 | Alan Roura | 99 | LA FABRIQUE | SUI07 | Brit Air, Votre Nom autour du Monde, MACSF | Groupe Finot-Conq | Multiplast | 01 Août 2007 | 18.28 | 5.90 | 4.5 | foils | 27.0 | carbone | 290.0 | 580.0 | |
| 25 | Thomas Ruyant | 113 | LinkedOut | NaN | NaN | Verdier | Persico | 03 Septembre 2019 | 18.28 | 5.85 | 4.5 | 8 | foils | 29.0 | acier forgé | 350.0 | 560.0 |
| 26 | Damien Seguin | 111 | GROUPE APICIL | FRA1000 | Comme Un Seul Homme, DCNS | Groupe Finot-Conq | Multiplast | 10 Août 2008 | 18.28 | 5.85 | 4.5 | 8.5 | 2 | 29.0 | acier forgé | 350.0 | 610.0 |
| 27 | Kojiro Shiraishi | 119 | DMG MORI Global One | JPN 11 | NaN | VPLP | Multiplast | 05 Septembre 2019 | 18.28 | 5.85 | 4.5 | 8 | foils | 29.0 | acier forgé | 320.0 | 580.0 |
| 28 | Sébastien Simon | 90 | ARKEA PAPREC | 4 | NaN | Juan Kouyoumdjian | CDK Technologies | 19 Juillet 2019 | 18.28 | 5.70 | 4.5 | 8 | foils | 29.0 | Inox usiné | 260.0 | 600.0 |
| 29 | Maxime Sorel | 107 | V and B-MAYENNE | FRA53 | Le Souffle du Nord, Groupe Bel | Verdier - VPLP | Indiana Yachting (Scarlino, Italie) | 07 Septembre 2007 | 18.28 | 5.50 | 4.5 | 7.7 | 2 | 29.0 | acier | 365.0 | 700.0 |
| 30 | Alex Thomson | 122 | HUGO BOSS | GBR 99 | NaN | VPLP - Alex Thomson Racing (led by Pete Hobson) | Carrington Boats | 15 Août 2019 | 18.28 | 5.40 | 4.5 | 7.7 | foils | 29.0 | acier forgé | 330.0 | 630.0 |
| 31 | Armel Tripon | 101 | L'OCCITANE EN PROVENCE | 2 | NaN | Samuel Manuard | Black Pepper© | 31 Janvier 2020 | 18.28 | 5.50 | 4.5 | 7.8 | foils | 28.0 | acier forgé | 270.0 | 535.0 |
| 32 | Nicolas Troussel | 108 | CORUM L'EPARGNE | FRA 6 | NaN | Juan Kouyoumdjian | CDK technologies - Mer Agitée | 15 Mai 2020 | 18.28 | 5.70 | 4.5 | 7.9 | foils | 27.3 | NaN | 270.0 | 535.0 |
Nous pouvons à présent combiner les dataframes des caractéristiques de bateau et de classement grâce au nom de skipper.
merge_df = pd.merge(left=df_running, right=df_boats, left_on="Skipper", right_on="skipper_name", how="left")
merge_df.drop(columns=['skipper_name', "Bateau"], inplace=True)
merge_df
| datetime | rank | Voile | Skipper | Latitude | Longitude | last30m_Cap | last30m_Vitesse | last30m_VMG | last30m_Distance | ... | Date de lancement | Longueur | Largeur | Tirant d'eau | Déplacement (poids) | Nombre de dérives | Hauteur mât | Voile quille | Surface de voiles au près | Surface de voiles au portant | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2020-11-08 14:00:00 | 1.0 | FRA 18 | Louis Burton | 46.407667 | -1.841333 | 241 | 17.7 | 17.5 | 0.3 | ... | 09 Juin 2015 | 18.28 | 5.80 | 4.5 | 7.6 | foils | 28.0 | acier | 300.0 | 600.0 |
| 1 | 2020-11-08 14:00:00 | 2.0 | MON 10 | Boris Herrmann | 46.405667 | -1.830333 | 241 | 11.1 | 10.9 | 0.4 | ... | 07 Août 2015 | 18.28 | 5.70 | 4.5 | 7.6 | foils | 29.0 | acier | 290.0 | 490.0 |
| 2 | 2020-11-08 14:00:00 | 3.0 | FRA 8 | Jérémie Beyou | 46.415167 | -1.833167 | 244 | 15.5 | 15.5 | 0.5 | ... | 18 Août 2018 | 18.28 | 5.85 | 4.5 | 8 | foils | 29.0 | acier | 320.0 | 600.0 |
| 3 | 2020-11-08 14:00:00 | 4.0 | FRA 59 | Thomas Ruyant | 46.411833 | -1.828000 | 244 | 13.2 | 13.1 | 0.7 | ... | 03 Septembre 2019 | 18.28 | 5.85 | 4.5 | 8 | foils | 29.0 | acier forgé | 350.0 | 560.0 |
| 4 | 2020-11-08 14:00:00 | 5.0 | FRA 53 | Maxime Sorel | 46.409833 | -1.826000 | 246 | 10.9 | 10.9 | 0.2 | ... | 07 Septembre 2007 | 18.28 | 5.50 | 4.5 | 7.7 | 2 | 29.0 | acier | 365.0 | 700.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 19222 | 2021-03-05 04:00:00 | NaN | FRA 109 | Samantha Davies | NaN | NaN | NaN | NaN | NaN | ... | 20 Septembre 2010 | 18.28 | 5.70 | 4.5 | 7.8 | foils | 27.0 | acier forgé | 300.0 | 600.0 | |
| 19223 | 2021-03-05 04:00:00 | NaN | FRA 4 | Sébastien Simon | NaN | NaN | NaN | NaN | NaN | ... | 19 Juillet 2019 | 18.28 | 5.70 | 4.5 | 8 | foils | 29.0 | Inox usiné | 260.0 | 600.0 | |
| 19224 | 2021-03-05 04:00:00 | NaN | GBR 99 | Alex Thomson | NaN | NaN | NaN | NaN | NaN | ... | 15 Août 2019 | 18.28 | 5.40 | 4.5 | 7.7 | foils | 29.0 | acier forgé | 330.0 | 630.0 | |
| 19225 | 2021-03-05 04:00:00 | NaN | FRA 85 | Kevin Escoffier | NaN | NaN | NaN | NaN | NaN | ... | 08 Mars 2010 | 18.28 | 5.50 | 4.5 | foils | 27.4 | Acier mécano soudé | 300.0 | 600.0 | ||
| 19226 | 2021-03-05 04:00:00 | NaN | FRA 6 | Nicolas Troussel | NaN | NaN | NaN | NaN | NaN | ... | 15 Mai 2020 | 18.28 | 5.70 | 4.5 | 7.9 | foils | 27.3 | NaN | 270.0 | 535.0 |
19227 rows × 36 columns
Il est possible d'afficher le classement final en récupérant les 33 dernières lignes du dataframe contenant les skipper ayant franchi la ligne d'arrivée. Les rang affichés en Nan correspondent aux abandons.
N.B. : Le classement final ne correspond pas exactement à l'ordre d'arrivée des skippers, certains temps de compensation ayant été accordés.
final_rank = df_finished[-33:][["Date d'arrivée", "Rang", "Skipper"]].reset_index(drop=True)
final_rank
| Date d'arrivée | Rang | Skipper | |
|---|---|---|---|
| 0 | 2021-01-28 04:19:46 | 1.0 | Yannick Bestaven |
| 1 | 2021-01-27 20:35:47 | 2.0 | Charlie Dalin |
| 2 | 2021-01-28 00:45:12 | 3.0 | Louis Burton |
| 3 | 2021-01-28 20:19:55 | 4.0 | Jean Le Cam |
| 4 | 2021-01-28 11:19:45 | 5.0 | Boris Herrmann |
| 5 | 2021-01-28 05:42:01 | 6.0 | Thomas Ruyant |
| 6 | 2021-01-28 12:18:20 | 7.0 | Damien Seguin |
| 7 | 2021-01-28 13:02:20 | 8.0 | Giancarlo Pedote |
| 8 | 2021-01-29 10:05:20 | 9.0 | Benjamin Dutreux |
| 9 | 2021-01-30 04:50:15 | 10.0 | Maxime Sorel |
| 10 | 2021-02-01 07:27:50 | 11.0 | Armel Tripon |
| 11 | 2021-02-03 16:44:25 | 12.0 | Clarisse Cremer |
| 12 | 2021-02-06 09:15:58 | 13.0 | Jérémie Beyou |
| 13 | 2021-02-06 17:06:02 | 14.0 | Romain Attanasio |
| 14 | 2021-02-11 08:56:06 | 15.0 | Arnaud Boissieres |
| 15 | 2021-02-11 11:52:56 | 16.0 | Kojiro Shiraishi |
| 16 | 2021-02-11 20:29:56 | 17.0 | Alan Roura |
| 17 | 2021-02-11 22:36:00 | 18.0 | Stéphane Le Diraison |
| 18 | 2021-02-12 01:57:30 | 19.0 | Pip Hare |
| 19 | 2021-02-13 20:47:03 | 20.0 | Didac Costa |
| 20 | 2021-02-16 10:28:31 | 21.0 | Clément Giraud |
| 21 | 2021-02-17 23:16:51 | 22.0 | Miranda Merron |
| 22 | 2021-02-20 08:35:40 | 23.0 | Manuel Cousin |
| 23 | 2021-02-28 07:23:44 | 24.0 | Alexia Barrier |
| 24 | 2021-03-05 08:35:46 | 25.0 | Ari Huusela |
| 25 | NaT | NaN | Sébastien Destremau |
| 26 | NaT | NaN | Isabelle Joschke |
| 27 | NaT | NaN | Fabrice Amedeo |
| 28 | NaT | NaN | Samantha Davies |
| 29 | NaT | NaN | Sébastien Simon |
| 30 | NaT | NaN | Alex Thomson |
| 31 | NaT | NaN | Kevin Escoffier |
| 32 | NaT | NaN | Nicolas Troussel |
A partir des données récupérées, il est à présent possible d'afficher la route de chacun des voiliers pendant la course.
fig = go.Figure()
df_skippers = []
for skipper in df_running["Skipper"].unique():
df_skippers.append(df_running[df_running["Skipper"] == skipper])
for df in df_skippers:
fig = fig.add_trace(go.Scattergeo(
lon = df['Longitude'],
lat = df['Latitude'],
text = df['datetime'].astype('str') + " : Classement " + df['rank'].astype('str'),
mode="lines",
name=df["Skipper"].unique()[0]))
fig.update_geos(projection_type="azimuthal equal area", fitbounds="locations")
fig.update_layout(height=600, margin={"r":0,"t":0,"l":0,"b":0})
# Option "svg" permet l'affichage de la carte sur GitHub ... mais enlève l'interactivité de la carte
# A remplacer par fig.show() pour rendre la carte interactive
fig.show("svg")
Cet affichage permet notamment de repérer l'abandon de certains skippers (Sébastien Destremeau qui s'arrête en Nouvelle-Zélande, Alex Thomson et Samantha Davies qui s'arrêtent au Cap de Bonne-Espérance, etc.)
En zoomant au sud du Cap de Bonne-Espérance, il est également possible d'observer le changement de cap de Yannick Bestaven (en bleu ci-dessou), qui a modifié sa trajectoire pour porter assistance à Kevin Escoffier (en rouge ci-dessous).
Par comparaison, Charlie Davin (en cyan) a maintenu son cap et terminé la course avant Yannick Bestaven. Un temps de compensation a par la suite été accordé à Yannick Bestaven pour le sauvetage, ce qui lui a permis de terminer 1er au classement.
fig = go.Figure()
df_skippers_shortlist = []
for skipper in ["Yannick Bestaven", "Kevin Escoffier", "Charlie Dalin"]:
df_skippers_shortlist.append(df_running[df_running["Skipper"] == skipper])
for df in df_skippers_shortlist:
fig = fig.add_trace(go.Scattergeo(
lon = df['Longitude'],
lat = df['Latitude'],
text = df['datetime'].astype('str') + " : Classement " + df['rank'].astype('str'),
mode="lines",
name=df["Skipper"].unique()[0]))
fig.update_geos(projection_type="natural earth", center=dict(lat=-41, lon=9), projection_scale=2.5)
fig.update_layout(height=300, margin={"r":0,"t":0,"l":0,"b":0})
fig.show("svg")
Il est également possible de visualiser les différences d'optimisation du trajet de chaque skipper. Sur la carte ci-dessous, on peut observer qu'Alan Roura (en cyan ci-dessou) a énormément dévié vers le continent américain sur le retour, et a donc parcouru beaucoup une distance total beaucoup plus importante que Maxime Sorel (en rouge ci-dessou) par exemple.
fig = go.Figure()
df_skippers_shortlist = []
for skipper in ["Charlie Dalin", "Maxime Sorel", "Alan Roura"]:
df_skippers_shortlist.append(df_running[df_running["Skipper"] == skipper])
for df in df_skippers_shortlist:
fig = fig.add_trace(go.Scattergeo(
lon = df['Longitude'],
lat = df['Latitude'],
text = df['datetime'].astype('str') + " : Classement " + df['rank'].astype('str'),
mode="lines",
name=df["Skipper"].unique()[0]))
fig.update_geos(projection_type="natural earth", fitbounds="locations")
fig.update_layout(height=300, margin={"r":0,"t":0,"l":0,"b":0})
fig.show("svg")
A partir des données des skippers encore en course, il est également possible de visualiser l'évolution du classement des skippers, ainsi que les dates des abandons. On remarque une grande variabilité des classements en début de course, mais la liste des skippers faisant partie du top 10 semble déjà être déterminée à partir du premier mois de course.
plt.figure(figsize=(20, 15))
for df in df_skippers:
df = df.dropna()
plt.plot(df["datetime"], df["rank"])
plt.text(df.iloc[-1]["datetime"] + pd.Timedelta(days=1), df.iloc[-1]["rank"]-0.2,
df["Skipper"].tolist()[0], fontsize=14)
plt.title('Evolution du classement au cours de la course', fontsize=16)
plt.xlabel('Date', fontsize=16)
plt.ylabel('Classement (avant correction avec temps compensatoire)', fontsize=16)
plt.yticks(range(34))
plt.show()
Il est également intéressant de regarder la distance totale parcourue par chaque skipper au long de la course.
plt.figure(figsize=(15, 10))
for df in df_skippers:
plt.plot(df["datetime"], df["last24h_Distance"].cumsum(), label=df["Skipper"].tolist()[0])
plt.legend(bbox_to_anchor=(1.18,0.98))
plt.grid(True, 'major', 'both')
plt.title('Distance cumulative parcourue par skipper')
plt.xlabel('Date')
plt.ylabel('Distance en miles nautiques')
plt.show()
On retrouve bien les observations faites sur la dernière carte présentée plus haut, à savoir que certains skippers comme Alan Roura ont parcouru des distances beaucoup plus importantes pour arriver à la ligne d'arrivée (ce qui impacte logiquement leur classement).
plt.figure(figsize=(10, 5))
for df in df_skippers:
if df["Skipper"].tolist()[0] in ["Charlie Dalin", "Maxime Sorel", "Alan Roura"]:
plt.plot(df["datetime"], df["last24h_Distance"].cumsum(), label=df["Skipper"].tolist()[0])
plt.legend()
plt.grid(True, 'major', 'both')
plt.title('Distance cumulative parcourue par skipper')
plt.xlabel('Date')
plt.ylabel('Distance en miles nautiques')
plt.show()
Après ces premières observations, il est à présent intéressant de se pencher sur les facteurs impactant le plus le classement final. On étudiera l'impact de la vitesse utile (VMG) des voiliers, et l'impact de la présence de foils sur le classement.
On calcule la VMG moyenne pour chaque skipper à partir des données relevées toutes les 24h pour effectuer une régression linéaire entre le classement et la VMG. Les données de VMG fournies par classement ne sont pas utilisées, car elles ne sont pas obtenues à des intervalles de temps constants.
df_moyennes = df_running.groupby("Skipper").mean()
df_moyennes.drop(columns=["Latitude", "Longitude", "last30m_Vitesse", "last30m_VMG", "last30m_Distance",
"last30m_Vitesse", "lastranking_Vitesse", "lastranking_VMG", "lastranking_Distance",
"lastranking_Vitesse"], inplace=True)
df_moyennes.rename(columns={"last24h_Vitesse": "Vitesse Moyenne", "last24h_VMG": "VMG Moyenne", "last24h_Distance":"Distance Moyenne", "rank": "Classement moyen"}, inplace=True)
df_moyennes
| Classement moyen | Vitesse Moyenne | VMG Moyenne | Distance Moyenne | DTF | DTL | |
|---|---|---|---|---|---|---|
| Skipper | ||||||
| Alan Roura | 16.466783 | 11.870455 | 10.641608 | 284.864510 | 12010.866608 | 1805.306294 |
| Alex Thomson | 7.461538 | 12.698718 | 10.966667 | 304.442949 | 20795.096154 | 336.194231 |
| Alexia Barrier | 24.819403 | 9.940000 | 9.077015 | 238.259403 | 12213.304627 | 3417.251493 |
| Ari Huusela | 25.955777 | 9.788873 | 8.673752 | 234.952211 | 11909.323966 | 3480.116262 |
| Armel Tripon | 16.076772 | 13.003150 | 11.930118 | 312.056496 | 12658.633268 | 1227.416929 |
| Arnaud Boissieres | 17.452297 | 11.969788 | 10.724558 | 287.263604 | 12175.901060 | 1990.906890 |
| Benjamin Dutreux | 7.572301 | 13.424644 | 12.376986 | 322.044196 | 12213.708758 | 397.569857 |
| Boris Herrmann | 6.921649 | 13.664124 | 12.542268 | 327.923093 | 12291.379588 | 330.507629 |
| Charlie Dalin | 2.286902 | 13.828898 | 12.604990 | 331.893971 | 12072.658004 | 63.314137 |
| Clarisse Cremer | 13.508604 | 12.624665 | 11.652199 | 302.536711 | 12036.986042 | 924.411855 |
| Clément Giraud | 24.295492 | 11.000835 | 10.073957 | 264.040067 | 12751.698998 | 2991.381970 |
| Damien Seguin | 6.382716 | 13.376337 | 12.493827 | 320.936420 | 12267.142387 | 330.776337 |
| Didac Costa | 19.964041 | 11.585445 | 10.358904 | 277.675171 | 12245.223116 | 2245.323459 |
| Fabrice Amedeo | 26.965000 | 9.860500 | 8.594000 | 236.703500 | 21319.035500 | 2020.559000 |
| Giancarlo Pedote | 10.423868 | 13.558642 | 12.516461 | 325.390329 | 12385.792593 | 449.426955 |
| Isabelle Joschke | 11.510638 | 13.260638 | 12.247340 | 318.225266 | 15182.419415 | 531.471809 |
| Jean Le Cam | 5.629098 | 13.364754 | 12.408402 | 320.729303 | 12223.731557 | 335.812705 |
| Jérémie Beyou | 21.430427 | 12.884972 | 11.228015 | 309.184045 | 13378.487570 | 2581.681447 |
| Kevin Escoffier | 5.846715 | 13.644526 | 12.180292 | 327.411679 | 21178.533577 | 230.286131 |
| Kojiro Shiraishi | 22.084063 | 12.014536 | 10.595972 | 288.328196 | 12782.084238 | 2558.718039 |
| Louis Burton | 6.809524 | 13.774741 | 12.580952 | 330.648447 | 12338.296273 | 328.187992 |
| Manuel Cousin | 21.302251 | 10.951286 | 9.707395 | 262.840675 | 11895.654019 | 2485.311897 |
| Maxime Sorel | 10.659274 | 13.094960 | 12.246371 | 313.839919 | 12282.564718 | 583.285887 |
| Miranda Merron | 23.522167 | 10.888013 | 9.932512 | 261.288670 | 12528.256650 | 2924.392775 |
| Nicolas Troussel | 7.260000 | 11.238000 | 9.376000 | 269.396000 | 23428.510000 | 81.250000 |
| Pip Hare | 19.221254 | 11.649826 | 10.537805 | 279.559582 | 12273.868118 | 2103.756794 |
| Romain Attanasio | 13.415896 | 12.082440 | 11.196673 | 289.994085 | 11836.016266 | 1078.734381 |
| Samantha Davies | 10.337423 | 12.824540 | 10.688344 | 306.887117 | 20697.139877 | 422.485890 |
| Stéphane Le Diraison | 17.818499 | 11.932810 | 10.596684 | 286.359860 | 12248.694066 | 2060.894241 |
| Sébastien Destremau | 27.872902 | 8.872182 | 7.811031 | 212.909353 | 17639.015827 | 4004.966187 |
| Sébastien Simon | 10.398734 | 13.120886 | 11.296835 | 314.924684 | 20763.519620 | 356.931013 |
| Thomas Ruyant | 3.524793 | 13.796281 | 12.539256 | 330.988017 | 12153.609091 | 168.134091 |
| Yannick Bestaven | 4.374741 | 13.644306 | 12.527329 | 327.464803 | 12165.802277 | 155.693168 |
plt.figure(figsize=(15, 5))
linreg = sns.regplot(x = "VMG Moyenne", y = "Classement moyen", data = df_moyennes)
plt.title("Régression linéaire entre le classement et la vitesse utile des voiliers", fontweight='bold')
plt.xlim([df_moyennes["VMG Moyenne"].min()-0.05, df_moyennes["VMG Moyenne"].max()+0.05])
plt.show()
Les paramètres de cette droite de régression sont les suivants :
slope, intercept, r_value, p_value, std_err = stats.linregress(x=linreg.get_lines()[0].get_xdata(),y=linreg.get_lines()[0].get_ydata())
reg = pd.DataFrame(columns=["Pente", "Ordonnée à l'origine", "r_value", "p_value", "Ecart-type"])
reg.loc[len(reg)] = [slope, intercept, r_value, p_value, std_err]
reg
| Pente | Ordonnée à l'origine | r_value | p_value | Ecart-type | |
|---|---|---|---|---|---|
| 0 | -4.713204 | 66.34955 | -1.0 | 0.0 | 1.003317e-08 |
coeff = np.corrcoef(df_moyennes["VMG Moyenne"], df_moyennes["Classement moyen"])[0][1]
print(f"Le coefficient de corrélation entre le classement et la VMG est de {coeff:.2f}")
Le coefficient de corrélation entre le classement et la VMG est de -0.83
Il existe donc bien une forte corrélation entre classement et vitesse utile. Les skippers avec le meilleur classement sont ceux qui ont la plus grande vitesse utile, en moyenne.
Pour mesurer l'impact des foils, on sépare le jeu de données en 2 groupes : skippers avec foils, et skippers sans foil. Pour chacun des groupes, on va traver la distribution des classements moyens et des VMG moyennes pour conclure.
moyennes_merge_df = pd.merge(left=df_moyennes, right=df_boats, left_on="Skipper", right_on="skipper_name", how="left")
moyennes_merge_df = moyennes_merge_df.replace("2 asymétriques", "2")
moyennes_merge_df
| Classement moyen | Vitesse Moyenne | VMG Moyenne | Distance Moyenne | DTF | DTL | skipper_name | skipper_id | Nom du bateau | Numéro de voile | ... | Date de lancement | Longueur | Largeur | Tirant d'eau | Déplacement (poids) | Nombre de dérives | Hauteur mât | Voile quille | Surface de voiles au près | Surface de voiles au portant | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 16.466783 | 11.870455 | 10.641608 | 284.864510 | 12010.866608 | 1805.306294 | Alan Roura | 99 | LA FABRIQUE | SUI07 | ... | 01 Août 2007 | 18.28 | 5.90 | 4.5 | foils | 27.0 | carbone | 290.0 | 580.0 | |
| 1 | 7.461538 | 12.698718 | 10.966667 | 304.442949 | 20795.096154 | 336.194231 | Alex Thomson | 122 | HUGO BOSS | GBR 99 | ... | 15 Août 2019 | 18.28 | 5.40 | 4.5 | 7.7 | foils | 29.0 | acier forgé | 330.0 | 630.0 |
| 2 | 24.819403 | 9.940000 | 9.077015 | 238.259403 | 12213.304627 | 3417.251493 | Alexia Barrier | 100 | TSE - 4MYPLANET | FRA72 | ... | 01 Mars 1998 | 18.28 | 5.54 | 4.5 | 9 | 2 | 29.0 | acier | 260.0 | 580.0 |
| 3 | 25.955777 | 9.788873 | 8.673752 | 234.952211 | 11909.323966 | 3480.116262 | Ari Huusela | 124 | STARK | FIN222 | ... | 06 Août 2007 | 18.28 | 5.80 | 4.5 | 8.5 | 2 | 28.0 | acier | 270.0 | 580.0 |
| 4 | 16.076772 | 13.003150 | 11.930118 | 312.056496 | 12658.633268 | 1227.416929 | Armel Tripon | 101 | L'OCCITANE EN PROVENCE | 2 | ... | 31 Janvier 2020 | 18.28 | 5.50 | 4.5 | 7.8 | foils | 28.0 | acier forgé | 270.0 | 535.0 |
| 5 | 17.452297 | 11.969788 | 10.724558 | 287.263604 | 12175.901060 | 1990.906890 | Arnaud Boissieres | 88 | LA MIE CÂLINE - ARTISANS ARTIPÔLE | FRA 14 | ... | 03 Août 2007 | 18.28 | 5.65 | 4.5 | 7.9 | foils | 29.0 | basculante avec vérin | 300.0 | 610.0 |
| 6 | 7.572301 | 13.424644 | 12.376986 | 322.044196 | 12213.708758 | 397.569857 | Benjamin Dutreux | 112 | OMIA - WATER FAMILY | FRA09 | ... | 03 Juillet 2007 | 18.28 | 5.85 | 4.5 | 8 | 2 | 29.0 | acier forgé | 300.0 | 700.0 |
| 7 | 6.921649 | 13.664124 | 12.542268 | 327.923093 | 12291.379588 | 330.507629 | Boris Herrmann | 109 | SEAEXPLORER - YACHT CLUB DE MONACO | 16 | ... | 07 Août 2015 | 18.28 | 5.70 | 4.5 | 7.6 | foils | 29.0 | acier | 290.0 | 490.0 |
| 8 | 2.286902 | 13.828898 | 12.604990 | 331.893971 | 12072.658004 | 63.314137 | Charlie Dalin | 105 | APIVIA | FRA 79 | ... | 05 Août 2019 | 18.28 | 5.85 | 4.5 | 8 | foils | 29.0 | acier | 350.0 | 560.0 |
| 9 | 13.508604 | 12.624665 | 11.652199 | 302.536711 | 12036.986042 | 924.411855 | Clarisse Cremer | 95 | BANQUE POPULAIRE X | FRA30 | ... | 01 Mars 2011 | 18.28 | 5.70 | 4.5 | 7.7 | 2 | 29.0 | acier forgé | 340.0 | 570.0 |
| 10 | 24.295492 | 11.000835 | 10.073957 | 264.040067 | 12751.698998 | 2991.381970 | Clément Giraud | 114 | Compagnie du Lit / Jiliti | FRA83 | ... | 26 Juillet 2006 | 18.28 | 5.75 | 4.5 | 8.5 | 2 | 29.0 | acier forgé | 300.0 | 620.0 |
| 11 | 6.382716 | 13.376337 | 12.493827 | 320.936420 | 12267.142387 | 330.776337 | Damien Seguin | 111 | GROUPE APICIL | FRA1000 | ... | 10 Août 2008 | 18.28 | 5.85 | 4.5 | 8.5 | 2 | 29.0 | acier forgé | 350.0 | 610.0 |
| 12 | 19.964041 | 11.585445 | 10.358904 | 277.675171 | 12245.223116 | 2245.323459 | Didac Costa | 123 | ONE PLANET ONE OCEAN | ESP 33 | ... | 02 Février 2000 | 18.28 | 5.30 | 4.5 | 8.9 | 2 | 26.0 | acier | 240.0 | 470.0 |
| 13 | 26.965000 | 9.860500 | 8.594000 | 236.703500 | 21319.035500 | 2020.559000 | Fabrice Amedeo | 93 | NEWREST - ART & FENÊTRES | FRA 56 | ... | 01 Août 2015 | 18.28 | 5.85 | 4.5 | 7 | foils | 29.0 | monotype | 320.0 | 570.0 |
| 14 | 10.423868 | 13.558642 | 12.516461 | 325.390329 | 12385.792593 | 449.426955 | Giancarlo Pedote | 96 | PRYSMIAN GROUP | ITA 34 | ... | 02 Avril 2015 | 18.28 | 5.80 | 4.5 | 8 | foils | 29.0 | acier forgé | 300.0 | 600.0 |
| 15 | 11.510638 | 13.260638 | 12.247340 | 318.225266 | 15182.419415 | 531.471809 | Isabelle Joschke | 102 | MACSF | FRA 27 | ... | 04 Juillet 2007 | 18.28 | 5.60 | 4.5 | 7.7 | foils | 27.0 | acier | 300.0 | 650.0 |
| 16 | 5.629098 | 13.364754 | 12.408402 | 320.729303 | 12223.731557 | 335.812705 | Jean Le Cam | 116 | Yes We Cam! | 001 | ... | 03 Janvier 2007 | 18.28 | 5.90 | 4.5 | 8 | 2 | 28.0 | acier forgé | 300.0 | 620.0 |
| 17 | 21.430427 | 12.884972 | 11.228015 | 309.184045 | 13378.487570 | 2581.681447 | Jérémie Beyou | 103 | CHARAL | 08 | ... | 18 Août 2018 | 18.28 | 5.85 | 4.5 | 8 | foils | 29.0 | acier | 320.0 | 600.0 |
| 18 | 5.846715 | 13.644526 | 12.180292 | 327.411679 | 21178.533577 | 230.286131 | Kevin Escoffier | 89 | PRB | FRA 85 | ... | 08 Mars 2010 | 18.28 | 5.50 | 4.5 | foils | 27.4 | Acier mécano soudé | 300.0 | 600.0 | |
| 19 | 22.084063 | 12.014536 | 10.595972 | 288.328196 | 12782.084238 | 2558.718039 | Kojiro Shiraishi | 119 | DMG MORI Global One | JPN 11 | ... | 05 Septembre 2019 | 18.28 | 5.85 | 4.5 | 8 | foils | 29.0 | acier forgé | 320.0 | 580.0 |
| 20 | 6.809524 | 13.774741 | 12.580952 | 330.648447 | 12338.296273 | 328.187992 | Louis Burton | 106 | BUREAU VALLEE 2 | 18 | ... | 09 Juin 2015 | 18.28 | 5.80 | 4.5 | 7.6 | foils | 28.0 | acier | 300.0 | 600.0 |
| 21 | 21.302251 | 10.951286 | 9.707395 | 262.840675 | 11895.654019 | 2485.311897 | Manuel Cousin | 92 | GROUPE SÉTIN | FRA 71 | ... | 02 Février 2007 | 18.28 | 5.80 | 4.5 | 9 | 2 | 28.5 | basculante sur vérin hydraulique | 270.0 | 560.0 |
| 22 | 10.659274 | 13.094960 | 12.246371 | 313.839919 | 12282.564718 | 583.285887 | Maxime Sorel | 107 | V and B-MAYENNE | FRA53 | ... | 07 Septembre 2007 | 18.28 | 5.50 | 4.5 | 7.7 | 2 | 29.0 | acier | 365.0 | 700.0 |
| 23 | 23.522167 | 10.888013 | 9.932512 | 261.288670 | 12528.256650 | 2924.392775 | Miranda Merron | 104 | CAMPAGNE DE FRANCE | FRA50 | ... | 04 Mai 2006 | 18.28 | 5.50 | 4.5 | 8.5 | 2 | 28.0 | carbone | 330.0 | 600.0 |
| 24 | 7.260000 | 11.238000 | 9.376000 | 269.396000 | 23428.510000 | 81.250000 | Nicolas Troussel | 108 | CORUM L'EPARGNE | FRA 6 | ... | 15 Mai 2020 | 18.28 | 5.70 | 4.5 | 7.9 | foils | 27.3 | NaN | 270.0 | 535.0 |
| 25 | 19.221254 | 11.649826 | 10.537805 | 279.559582 | 12273.868118 | 2103.756794 | Pip Hare | 120 | MEDALLIA | GBR77 | ... | 03 Juillet 1999 | 18.28 | 5.70 | 4.5 | 9 | 2 | 29.0 | carbone | 300.0 | 580.0 |
| 26 | 13.415896 | 12.082440 | 11.196673 | 289.994085 | 11836.016266 | 1078.734381 | Romain Attanasio | 110 | PURE - Best Western® | FRA 49 | ... | 08 Mars 2007 | 18.28 | 5.80 | 4.5 | 9 | 2 | 28.0 | acier forgé | 280.0 | 560.0 |
| 27 | 10.337423 | 12.824540 | 10.688344 | 306.887117 | 20697.139877 | 422.485890 | Samantha Davies | 97 | INITIATIVES-COEUR | FRA109 | ... | 20 Septembre 2010 | 18.28 | 5.70 | 4.5 | 7.8 | foils | 27.0 | acier forgé | 300.0 | 600.0 |
| 28 | 17.818499 | 11.932810 | 10.596684 | 286.359860 | 12248.694066 | 2060.894241 | Stéphane Le Diraison | 94 | TIME FOR OCEANS | FRA 92 | ... | 01 Juin 2007 | 18.28 | 5.84 | 4.5 | 8.5 | foils | 28.0 | monotype | 300.0 | 550.0 |
| 29 | 27.872902 | 8.872182 | 7.811031 | 212.909353 | 17639.015827 | 4004.966187 | Sébastien Destremau | 117 | MERCI | 69 | ... | 15 Janvier 2005 | 18.28 | 5.60 | 4.5 | 8.5 | 2 | 29.0 | acier | 250.0 | 650.0 |
| 30 | 10.398734 | 13.120886 | 11.296835 | 314.924684 | 20763.519620 | 356.931013 | Sébastien Simon | 90 | ARKEA PAPREC | 4 | ... | 19 Juillet 2019 | 18.28 | 5.70 | 4.5 | 8 | foils | 29.0 | Inox usiné | 260.0 | 600.0 |
| 31 | 3.524793 | 13.796281 | 12.539256 | 330.988017 | 12153.609091 | 168.134091 | Thomas Ruyant | 113 | LinkedOut | NaN | ... | 03 Septembre 2019 | 18.28 | 5.85 | 4.5 | 8 | foils | 29.0 | acier forgé | 350.0 | 560.0 |
| 32 | 4.374741 | 13.644306 | 12.527329 | 327.464803 | 12165.802277 | 155.693168 | Yannick Bestaven | 91 | Maître CoQ IV | 17 | ... | 12 Mars 2015 | 18.28 | 5.80 | 4.5 | 8 | foils | 29.0 | acier mécano soudé | 310.0 | 550.0 |
33 rows × 23 columns
foils = moyennes_merge_df[moyennes_merge_df["Nombre de dérives"] == "foils"]
no_foil = moyennes_merge_df[moyennes_merge_df["Nombre de dérives"] != "foils"]
plt.figure(figsize=(8, 4))
sns.kdeplot(foils["Classement moyen"], bw=0.3, label="Avec foils", fill=True)
plt.axvline(x = foils['Classement moyen'].mean(), color = 'C0', label ="Classement moyen avec foils", linestyle="dotted")
sns.kdeplot(no_foil["Classement moyen"], bw=0.3, label="Sans foil", fill=True)
plt.axvline(x = no_foil['Classement moyen'].mean(), color = 'C1', label ="Classement moyen sans foil", linestyle="dotted")
plt.title("Densité de répartition du classement des skippers avec/sans foil sur leur voilier")
plt.legend(loc="best")
plt.show()
Valeurs numériques
print(f"Classement moyen avec foils : {foils['Classement moyen'].mean():.2f}")
print(f"Classement moyen sans foil : {no_foil['Classement moyen'].mean():.2f}")
print(f"Ecart-type du classement avec foils : {foils['Classement moyen'].std():.2f}")
print(f"Ecart-type du classement sans foil : {no_foil['Classement moyen'].std():.2f}")
Classement moyen avec foils : 11.87 Classement moyen sans foil : 17.44 Ecart-type du classement avec foils : 6.99 Ecart-type du classement sans foil : 7.75
On remarque donc, qu'à dispertion similaire, le classement moyen des skippers avec foils est bien inférieur à celui des skippers sans foil !
plt.figure(figsize=(8, 4))
sns.kdeplot(foils["VMG Moyenne"], bw=0.4, label="Avec foils", fill=True)
plt.axvline(x = foils["VMG Moyenne"].mean(), color = 'C0', label ="VMG moyenne avec foils", linestyle="dotted")
sns.kdeplot(no_foil["VMG Moyenne"], bw=0.4, label="Sans foil", fill=True)
plt.axvline(x = no_foil["VMG Moyenne"].mean(), color = 'C1', label ="VMG moyenne sans foil", linestyle="dotted")
plt.title("Densité de répartition de VMG des skippers avec/sans foil sur leur voilier")
plt.legend(loc="upper left")
plt.show()
print(f"VMG moyenne avec foils : {foils['VMG Moyenne'].mean():.2f}")
print(f"VMG moyenne sans foil : {no_foil['VMG Moyenne'].mean():.2f}")
print(f"Ecart-type de la VMG avec foils : {foils['VMG Moyenne'].std():.2f}")
print(f"Ecart-type de la VMG sans foil : {no_foil['VMG Moyenne'].std():.2f}")
VMG moyenne avec foils : 11.39 VMG moyenne sans foil : 10.61 Ecart-type de la VMG avec foils : 1.17 Ecart-type de la VMG sans foil : 1.51
De même que pour les classements, on remarque donc, qu'à dispertion similaire, la VMG des skippers avec foils est bien supérieure à celle des skippers sans foil !
On peut retrouver ces résultats sur la courbe de comparaison classement / VMG sur le graphe ci-dessous
sns.jointplot(data=moyennes_merge_df, x="Classement moyen", y="VMG Moyenne", hue="Nombre de dérives", height=6, kind="scatter")
plt.show()
La présence de foils sur le voilier semble donc bien être un facteur favorisant un bon classement.
Les données sont fournies à intervalles temporels réguliers, il est donc intéressant de se poser la question de leur analyse temporelle. Intéressons-nous aux données d'un participant en particulier pour essayer de trouver des caractéristiques intéressantes, Yannick Bestave par exemple.
Visualisation de l'évolution du rang et de la VMG au long de la course
yan = df_running[df_running["Skipper"] == "Yannick Bestaven"].reset_index(drop=True)
plt.rcParams['figure.figsize'] = [18, 4]
fig, axs = plt.subplots(1, 2)
axs[0].plot(yan["datetime"],yan["rank"])
axs[0].set_xlabel("Date")
axs[0].set_ylabel("Rang")
axs[0].axhline(y=yan["rank"].mean(), label="Classement moyen", color="C1")
axs[0].legend()
axs[1].plot(yan["datetime"],yan["last24h_VMG"])
axs[1].set_xlabel("Date")
axs[1].set_ylabel("VMG")
axs[1].axhline(y=yan["last24h_VMG"].mean(), label="VMG moyenne", color="C1")
axs[1].legend()
plt.show()
Il ne semble pas y avoir de saisonnalité dans les données de classement et de VMG (caractéristiques qui nous intéressent le plus).
Y a-t-il un lien entre le rang à un instant $t$ et celui à un instant $t+1$ ? entre la VMG à un instant $t$ et celle à un instant $t+1$? Traçons quelques lag plots pour estimer cela.
De gauche à droite sont représentés le rang à un instant $t+1$, $t+2$, $t+3$, $t+4$ et $t+5$ à partir du rang à un instant $t$.
plt.rcParams['figure.figsize'] = [20, 4]
fig, axs = plt.subplots(1, 5)
pd.plotting.lag_plot(yan["rank"], lag=1, ax=axs[0])
pd.plotting.lag_plot(yan["rank"], lag=2, ax=axs[1])
pd.plotting.lag_plot(yan["rank"], lag=3, ax=axs[2])
pd.plotting.lag_plot(yan["rank"], lag=4, ax=axs[3])
pd.plotting.lag_plot(yan["rank"], lag=5, ax=axs[4])
plt.show()
On observe bien une corrélation entre le rang à un instant $t$ et $t+1$, corrélation qui diminue lorsqu'on augmente le lag. On observe également plus de dispersion aux alentours des rangs élevés, cette corrélation est donc probablement meilleure pour les meilleurs skippers.
plt.rcParams['figure.figsize'] = [15, 4]
fig, axs = plt.subplots(1, 4)
pd.plotting.lag_plot(yan["last24h_VMG"], lag=1, ax=axs[0])
axs[0].set_title("Lag = 1, VMG sur 24h")
pd.plotting.lag_plot(yan["last24h_VMG"], lag=2, ax=axs[1])
axs[1].set_title("Lag = 2, VMG sur 24h")
pd.plotting.lag_plot(yan["lastranking_VMG"], lag=1, ax=axs[2])
axs[2].set_title("Lag = 1, VMG last ranking")
pd.plotting.lag_plot(yan["lastranking_VMG"], lag=2, ax=axs[3])
axs[3].set_title("Lag = 2, VMG last ranking")
plt.show()
Au niveau de la VMG, on observe une corrélation encore plus forte qu'avec le rang. Cette corrélatione est bien meilleure lorsqu'on utilise la VMG calculée sur 24h plutôt qu'entre chaque classement.
La fonction plot_acf de la libray statsmodels permet de tracer la fonction d'autocorrélation (ACF) et la fonction d'autocorrélation partielle (PACF) des variables souhaitées (importance des variables aux instants t-1, t-2, ... t-n sur la variable à l'instant t). Sur les graphes ci-dessous, les points dans les zones bleues sont sous le seuil significatif, et sont donc considérés comme n'ayant pas de poids sur l'observation à l'instant t.
plt.rcParams['figure.figsize'] = [15, 4]
fig, axs = plt.subplots(1, 2)
plot_acf(yan["rank"], ax=axs[0]);
axs[0].set_title("ACF du classement")
plot_acf(yan["last24h_VMG"], ax=axs[1]);
axs[1].set_title("ACF de la VMG")
plt.show()
Le classement ne semble donc pas idéal pour appliquer un modèle ARIMA, mais cela semble possible avec la VMG.
Vérifions cela de manière plus formelle avec un test augmenté de Dickey-Fuller.
plt.rcParams['figure.figsize'] = [15, 4]
fig, axs = plt.subplots(1, 2)
plot_pacf(yan["rank"], ax=axs[0]);
axs[0].set_title("PACF du classement")
plot_pacf(yan["last24h_VMG"], ax=axs[1]);
axs[1].set_title("PACF de la VMG")
plt.show()
La PACF révèle que pour la VMG, il existe une forte corrélation entre la VMG à un instant t, t-1 et également t-2.
def check_stationarity(series):
result = adfuller(series.values)
print('ADF Statistic: %f' % result[0])
print('p-value: %f' % result[1])
print('Critical Values:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
if (result[1] <= 0.05) & (result[4]['5%'] > result[0]):
print("\u001b[32mStationary\u001b[0m")
else:
print("\x1b[31mNon-stationary\x1b[0m")
check_stationarity(yan["rank"])
ADF Statistic: -1.385071
p-value: 0.589412
Critical Values:
1%: -3.444
5%: -2.868
10%: -2.570
Non-stationary
Appliquer un modèle ARIMA au classement n'est donc pas pertinent, pour ce skipper tout du moins.
check_stationarity(yan["last24h_VMG"])
ADF Statistic: -3.662662
p-value: 0.004670
Critical Values:
1%: -3.444
5%: -2.868
10%: -2.570
Stationary
Au vu des résultats, nous allons essayer d'appliquer un modèle ARIMA pour prédire la VMG du skipper Yannick Bestaven
Les paramètres p et q à choisir dépendent des résultats de l'ACF et PACF :
print(f"Nombre de classements où Yannick Bestaven est encore en course : {len(yan['datetime'].unique().tolist())}")
Nombre de classements où Yannick Bestaven est encore en course : 483
arima1 = ARIMA(yan["last24h_VMG"][:300], order=(2,0,13))
arima2 = ARIMA(yan["last24h_VMG"][:300], order=(7,0,13))
model1 = arima1.fit()
model2 = arima2.fit()
model1.summary()
| Dep. Variable: | last24h_VMG | No. Observations: | 300 |
|---|---|---|---|
| Model: | ARIMA(2, 0, 13) | Log Likelihood | -314.466 |
| Date: | Sun, 04 Dec 2022 | AIC | 662.932 |
| Time: | 23:34:07 | BIC | 725.896 |
| Sample: | 0 | HQIC | 688.130 |
| - 300 | |||
| Covariance Type: | opg |
| coef | std err | z | P>|z| | [0.025 | 0.975] | |
|---|---|---|---|---|---|---|
| const | 12.4261 | 1.691 | 7.347 | 0.000 | 9.111 | 15.741 |
| ar.L1 | 1.2978 | 1.731 | 0.750 | 0.453 | -2.095 | 4.690 |
| ar.L2 | -0.3294 | 1.628 | -0.202 | 0.840 | -3.521 | 2.862 |
| ma.L1 | 0.2551 | 1.735 | 0.147 | 0.883 | -3.145 | 3.655 |
| ma.L2 | 0.1189 | 1.058 | 0.112 | 0.911 | -1.955 | 2.193 |
| ma.L3 | 0.2833 | 0.608 | 0.466 | 0.641 | -0.908 | 1.475 |
| ma.L4 | 0.1523 | 0.736 | 0.207 | 0.836 | -1.290 | 1.595 |
| ma.L5 | 0.0727 | 0.562 | 0.129 | 0.897 | -1.028 | 1.174 |
| ma.L6 | -0.5645 | 0.362 | -1.561 | 0.119 | -1.273 | 0.144 |
| ma.L7 | -0.0041 | 0.825 | -0.005 | 0.996 | -1.622 | 1.614 |
| ma.L8 | 0.0249 | 0.344 | 0.072 | 0.942 | -0.649 | 0.699 |
| ma.L9 | -0.0562 | 0.102 | -0.549 | 0.583 | -0.257 | 0.144 |
| ma.L10 | 0.0680 | 0.127 | 0.535 | 0.593 | -0.181 | 0.317 |
| ma.L11 | 0.0612 | 0.159 | 0.386 | 0.700 | -0.250 | 0.372 |
| ma.L12 | -0.1313 | 0.193 | -0.680 | 0.497 | -0.510 | 0.247 |
| ma.L13 | -0.0323 | 0.168 | -0.192 | 0.848 | -0.361 | 0.297 |
| sigma2 | 0.4589 | 0.029 | 15.715 | 0.000 | 0.402 | 0.516 |
| Ljung-Box (L1) (Q): | 0.21 | Jarque-Bera (JB): | 342.81 |
|---|---|---|---|
| Prob(Q): | 0.64 | Prob(JB): | 0.00 |
| Heteroskedasticity (H): | 0.77 | Skew: | -1.14 |
| Prob(H) (two-sided): | 0.19 | Kurtosis: | 7.72 |
model2.summary()
| Dep. Variable: | last24h_VMG | No. Observations: | 300 |
|---|---|---|---|
| Model: | ARIMA(7, 0, 13) | Log Likelihood | -311.786 |
| Date: | Sun, 04 Dec 2022 | AIC | 667.572 |
| Time: | 23:34:08 | BIC | 749.055 |
| Sample: | 0 | HQIC | 700.182 |
| - 300 | |||
| Covariance Type: | opg |
| coef | std err | z | P>|z| | [0.025 | 0.975] | |
|---|---|---|---|---|---|---|
| const | 12.4453 | 1.672 | 7.444 | 0.000 | 9.169 | 15.722 |
| ar.L1 | 1.5751 | 0.499 | 3.156 | 0.002 | 0.597 | 2.553 |
| ar.L2 | -0.7418 | 0.965 | -0.769 | 0.442 | -2.633 | 1.149 |
| ar.L3 | 0.1411 | 0.828 | 0.171 | 0.865 | -1.481 | 1.763 |
| ar.L4 | -0.3255 | 0.747 | -0.436 | 0.663 | -1.789 | 1.138 |
| ar.L5 | 0.5716 | 0.719 | 0.795 | 0.427 | -0.837 | 1.981 |
| ar.L6 | -0.7531 | 0.625 | -1.206 | 0.228 | -1.977 | 0.471 |
| ar.L7 | 0.4759 | 0.284 | 1.678 | 0.093 | -0.080 | 1.032 |
| ma.L1 | -0.0402 | 0.507 | -0.079 | 0.937 | -1.034 | 0.954 |
| ma.L2 | 0.0939 | 0.329 | 0.285 | 0.775 | -0.551 | 0.739 |
| ma.L3 | 0.2651 | 0.295 | 0.899 | 0.369 | -0.313 | 0.843 |
| ma.L4 | 0.4145 | 0.309 | 1.340 | 0.180 | -0.192 | 1.021 |
| ma.L5 | -0.0087 | 0.271 | -0.032 | 0.974 | -0.539 | 0.522 |
| ma.L6 | -0.1145 | 0.235 | -0.487 | 0.627 | -0.576 | 0.347 |
| ma.L7 | 0.5527 | 0.254 | 2.174 | 0.030 | 0.054 | 1.051 |
| ma.L8 | 0.1834 | 0.294 | 0.624 | 0.532 | -0.392 | 0.759 |
| ma.L9 | 0.1278 | 0.259 | 0.493 | 0.622 | -0.380 | 0.636 |
| ma.L10 | 0.0243 | 0.244 | 0.099 | 0.921 | -0.455 | 0.503 |
| ma.L11 | 0.2427 | 0.254 | 0.957 | 0.339 | -0.254 | 0.740 |
| ma.L12 | -0.3794 | 0.128 | -2.964 | 0.003 | -0.630 | -0.129 |
| ma.L13 | -0.1209 | 0.167 | -0.723 | 0.470 | -0.449 | 0.207 |
| sigma2 | 0.4493 | 0.030 | 14.889 | 0.000 | 0.390 | 0.508 |
| Ljung-Box (L1) (Q): | 0.23 | Jarque-Bera (JB): | 324.17 |
|---|---|---|---|
| Prob(Q): | 0.63 | Prob(JB): | 0.00 |
| Heteroskedasticity (H): | 0.78 | Skew: | -1.19 |
| Prob(H) (two-sided): | 0.22 | Kurtosis: | 7.50 |
Les coefficients AIC, BIC et HQIC sont plus faibles sur le modèle 1 (avec p=2). On retient donc ce modèle pour nos prévisions.
Le graphe ci-dessous affiche les valeurs restituées par le modèle sur les données d'apprentissage en orange, et celles prédites pour les 183 classements restants en vert.
On remarque que le modèle a bien appris les valeurs : la courbe réelle est toujours incluse dans l'intervalle de confiance à 95%. En revanche, les valeurs prédites divergent très vite des valeurs réelles...
forecast = model1.forecast(183)
plt.rcParams['figure.figsize'] = [20, 8]
fig, ax = plt.subplots()
ax.plot(yan["last24h_VMG"], label="VMG réelle")
plot_predict(model1, ax=ax)
ax.plot(np.arange(300,483), forecast, label="VMG prédite par le modèle")
plt.legend()
plt.show()
Pour quantifier cela un peu mieux, on peut tracer le carré de l'erreur de prédiction. On observe que le modèle est plutôt bon sur les 15 premières prédictions, valeur à partir de laquelle il diverge.
plt.rcParams['figure.figsize'] = [15, 6]
error = yan["last24h_VMG"][300:].to_numpy() - forecast
plt.plot(error**2, label="Erreur de prédiction au carré")
plt.axvline(x = 315, color ='C1', linestyle="dotted", label="15 relevés de classement après la dernière valeur apprise")
plt.legend()
plt.show()
On peut essayer d'entraîner le modèle sur plus de points pour voir s'il arrive à réaliser des prédictions à plus longue portée : entraînement sur 400 points et 450 points (puis prédiction sur 83 et 33 points).
arima400 = ARIMA(yan["last24h_VMG"][:400], order=(2,0,13))
model400 = arima400.fit()
arima450 = ARIMA(yan["last24h_VMG"][:450], order=(2,0,13))
model450 = arima450.fit()
forecast400 = model400.forecast(83)
forecast450 = model450.forecast(33)
plt.rcParams['figure.figsize'] = [25, 6]
fig, ax = plt.subplots(1, 2)
fig.subplots_adjust(wspace=0, hspace=0)
ax[0].plot(yan["last24h_VMG"], label="VMG réelle")
plot_predict(model400, ax=ax[0])
ax[0].plot(np.arange(400,483), forecast400, label="VMG prédite par le modèle")
ax[0].legend()
ax[1].plot(yan["last24h_VMG"], label="VMG réelle")
plot_predict(model450, ax=ax[1])
ax[1].plot(np.arange(450,483), forecast450, label="VMG prédite par le modèle")
ax[1].legend()
plt.show()
plt.rcParams['figure.figsize'] = [15, 4]
fig, ax = plt.subplots(1, 2)
error400 = yan["last24h_VMG"][400:].to_numpy() - forecast400
error450 = yan["last24h_VMG"][450:].to_numpy() - forecast450
ax[0].plot(error400**2, label="Erreur de prédiction au carré")
ax[1].plot(error450**2, label="Erreur de prédiction au carré")
ax[0].axvline(x = 415, color ='C1', linestyle="dotted", label="15 relevés de classement après la dernière valeur apprise")
ax[1].axvline(x = 465, color ='C1', linestyle="dotted", label="15 relevés de classement après la dernière valeur apprise")
ax[0].legend()
ax[1].legend()
plt.show()
On ne remarque pas d'amélioration notable des prédictions (même une déterioration).
Ceci est prévisible, car il n'y a pas de saisonnalité dans les données pouvant aider les prédictions. De ce point de vue, une prédiction de vitesse 15 relevés de classement en avance est déjà un résultat intéressant.
On peut également regarder les valeurs de rang d'autres skippers pour voir si certains sont stationnaires. Prenons l'exemple de Maxime Sorel
sor = df_running[df_running["Skipper"] == "Maxime Sorel"].reset_index(drop=True)
plt.rcParams['figure.figsize'] = [18, 4]
fig, axs = plt.subplots(1, 2)
axs[0].plot(sor["datetime"],sor["rank"])
axs[0].set_xlabel("Date")
axs[0].set_ylabel("Rang")
axs[0].axhline(y=sor["rank"].mean(), label="Classement moyen", color="C1")
axs[0].legend()
axs[1].plot(sor["datetime"],sor["last24h_VMG"])
axs[1].set_xlabel("Date")
axs[1].set_ylabel("VMG")
axs[1].axhline(y=sor["last24h_VMG"].mean(), label="VMG moyenne", color="C1")
axs[1].legend()
plt.show()
plt.rcParams['figure.figsize'] = [15, 4]
fig, axs = plt.subplots(1, 2)
plot_acf(sor["rank"], ax=axs[0]);
axs[0].set_title("ACF du classement")
plot_acf(sor["last24h_VMG"], ax=axs[1]);
axs[1].set_title("ACF de la VMG")
plt.show()
plt.rcParams['figure.figsize'] = [15, 4]
fig, axs = plt.subplots(1, 2)
plot_pacf(sor["rank"], ax=axs[0]);
axs[0].set_title("PACF du classement")
plot_pacf(sor["last24h_VMG"], ax=axs[1]);
axs[1].set_title("PACF de la VMG")
plt.show()
check_stationarity(sor["rank"])
ADF Statistic: -4.673052
p-value: 0.000095
Critical Values:
1%: -3.444
5%: -2.868
10%: -2.570
Stationary
Cette fois, le rang est stationnaire. On peut essayer de prédire les futurs classement de Maxime Sorel par la même méthode que précédemment.
D'après les graphes ci-dessus, on choisit les valeurs suivantes pour les paramètres d'arima:
print(f"Nombre de classements où Maxime Sorel est encore en course : {len(sor['datetime'].unique().tolist())}")
Nombre de classements où Maxime Sorel est encore en course : 496
arima = ARIMA(sor["rank"][:300], order=(4,0,19))
model = arima.fit()
forecast = model.forecast(196)
plt.rcParams['figure.figsize'] = [10, 4]
fig, ax = plt.subplots()
ax.plot(sor["rank"], label="Classement réel")
plot_predict(model, ax=ax)
ax.plot(np.arange(300,496), forecast, label="Classement prédit par le modèle")
plt.legend()
plt.show()
print(f"Dernière valeur prédite par le modèle : {np.round(forecast.iloc[-1])}")
print(f"Dernière classement réel de Maxime Sorel : {sor['rank'].iloc[-1]}")
Dernière valeur prédite par le modèle : 11.0 Dernière classement réel de Maxime Sorel : 10.0
Le modèle ne prédit par la valeur exacte, mais n'est pas très loin du vrai résultat.
Par curiosité, tentons également de prédire la VMG de ce skipper avec les paramètre ARIMA suivants :
arima = ARIMA(sor["last24h_VMG"][:300], order=(4,0,8))
model = arima.fit()
forecast = model.forecast(196)
plt.rcParams['figure.figsize'] = [10, 4]
fig, ax = plt.subplots()
ax.plot(sor["last24h_VMG"], label="VMG réelle")
plot_predict(model, ax=ax)
ax.plot(np.arange(300,496), forecast, label="VMG prédite par le modèle")
plt.legend()
plt.show()